﻿using Cognitum.CassandraTableStorage;
using Cognitum.CassandraTableStorage.Entities;
using Cognitum.EmailSenders;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
using System.Configuration.Provider;
using System.Data.Services.Client;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Web.Configuration;
using System.Web.Security;

namespace Cognitum.CassandraProviders.Cassandra
{
    public class CassandraMembershipProvider : MembershipProvider
    {
        #region Structs

        private struct DefaultValues
        {
            #region Fields

            public const int Gender = -1;
            public const bool IsApproved = false;
            public const bool IsDeleted = false;
            public const bool IsLockedOut = false;

            #endregion Fields
        }

        #endregion Structs

        #region | Private Fields |

        private int _replicationFactor;

        private string _connectionString;

        //User active window time
        private int _userIsActiveTimeWindow;

        private int _userIsOnlineTimeWindow;

        /// Application name
        private string applicationName;

        // Enable - disable password reseting
        private bool enablePasswordReset;

        // Enable - disable password retrieval
        private bool enablePasswordRetrieval;

        private MachineKeySection machineKey;

        // Max number of failed password attempts before the account is blocked, and time to reset that counter
        private int maxInvalidPasswordAttempts;

        // Minium non-alphanumeric char required
        private int minRequiredNonAlphanumericCharacters = 0;

        // Minimun password length
        private int minRequiredPasswordLength = 6;

        private int passwordAttemptWindow;
        private MembershipPasswordFormat passwordFormat;

        // Regular expression the password should match (empty for none)
        private string passwordStrengthRegularExpression;

        /// Require security question and answer (this, for instance, is a functionality which not many people use)
        private bool requiresQuestionAndAnswer;

        // Require email to be unique
        private bool requiresUniqueEmail;

        #endregion | Private Fields |

        #region | Public Properties |

        public override string ApplicationName
        {
            get { return applicationName; }
            set { applicationName = value; }
        }

        public override bool EnablePasswordReset
        {
            get { return enablePasswordReset; }
        }

        public override bool EnablePasswordRetrieval
        {
            get { return enablePasswordRetrieval; }
        }

        public override int MaxInvalidPasswordAttempts
        {
            get { return maxInvalidPasswordAttempts; }
        }

        public override int MinRequiredNonAlphanumericCharacters
        {
            get { return minRequiredNonAlphanumericCharacters; }
        }

        public override int MinRequiredPasswordLength
        {
            get { return minRequiredPasswordLength; }
        }

        public override int PasswordAttemptWindow
        {
            get { return passwordAttemptWindow; }
        }

        public override MembershipPasswordFormat PasswordFormat
        {
            get { return passwordFormat; }
        }

        public override string PasswordStrengthRegularExpression
        {
            get { return passwordStrengthRegularExpression; }
        }

        public override bool RequiresQuestionAndAnswer
        {
            get { return requiresQuestionAndAnswer; }
        }

        public override bool RequiresUniqueEmail
        {
            get { return requiresUniqueEmail; }
        }

        #endregion | Public Properties |

        #region | Public Methods |

        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            username = username.ToLower();
            if (!ValidateUser(username, oldPassword)) return false;

            var args = new ValidatePasswordEventArgs(username, newPassword, true);

            OnValidatingPassword(args);

            if (args.Cancel)
                if (args.FailureInformation != null)
                    throw args.FailureInformation;
                else
                    throw new MembershipPasswordException("Change password canceled due to new password validation failure.");

            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            IEnumerable<user_entity> userEntities = userDataSource.GetUser(GetPartitionKeyFromUsername(username).ToString(), applicationName);

            user_entity userEntity = userEntities.FirstOrDefault();
            if (userEntity == null) return false;

            userEntity.password = EncodePassword(newPassword);
            userEntity.last_password_changed_date = userEntity.last_activity_date = DateTimeOffset.Now;
            userDataSource.Update(userEntity, _userIsOnlineTimeWindow, _userIsActiveTimeWindow);

            return true;
        }

        public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
        {
            username = username.ToLower();
            if (!ValidateUser(username, password)) return false;

            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            IEnumerable<user_entity> userEntities = userDataSource.GetUser(GetPartitionKeyFromUsername(username).ToString(), applicationName);

            user_entity userEntity = userEntities.FirstOrDefault();
            if (userEntity == null) return false;

            userEntity.password_question = newPasswordQuestion;
            userEntity.password_answer = newPasswordAnswer;
            userEntity.last_activity_date = DateTimeOffset.Now;
            userDataSource.Update(userEntity, _userIsOnlineTimeWindow, _userIsActiveTimeWindow);

            return true;
        }

        public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
        {
            username = username.ToLower();

            #region Validate password

            var args = new ValidatePasswordEventArgs(username, password, true);

            OnValidatingPassword(args);

            if (args.Cancel)
            {
                status = MembershipCreateStatus.InvalidPassword;
                return null;
            }

            if (RequiresUniqueEmail && GetUserNameByEmail(email) != String.Empty)
            {
                status = MembershipCreateStatus.DuplicateEmail;
                return null;
            }

            #endregion Validate password

            if (GetUser(username, false) != null)
            {
                status = MembershipCreateStatus.DuplicateUserName;
                return null;
            }

            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            try
            {
                userDataSource.Insert(new user_entity(GetPartitionKeyFromUsername(username).ToString())
                {
                    username = username,
                    password = EncodePassword(password),
                    creation_date = DateTimeOffset.Now,
                    is_deleted = DefaultValues.IsDeleted,
                    is_locked_out = DefaultValues.IsLockedOut,
                    is_approved = isApproved,
                    application_name = applicationName,
                    email = email,
                    password_question = passwordQuestion,
                    password_answer = passwordAnswer,
                    gender = DefaultValues.Gender,
                    last_profile_updated_date = DateTimeOffset.Now,
                    last_activity_date = DateTimeOffset.Now,
                    last_password_changed_date = DateTimeOffset.Now,
                    last_login_date = new DateTimeOffset(new DateTime(1970, 1, 1)),
                    last_locked_out_date = new DateTimeOffset(new DateTime(1970, 1, 1)),
                    failed_password_attempt_window_start = new DateTimeOffset(new DateTime(1970, 1, 1)),
                    failed_password_answer_attempt_window_start = new DateTimeOffset(new DateTime(1970, 1, 1)),
                    birthday = new DateTimeOffset(new DateTime(1970, 1, 1)),
                    is_profile_deleted = false
                }, _userIsOnlineTimeWindow, _userIsActiveTimeWindow);
            }
            catch (DataServiceRequestException)
            {
                status = MembershipCreateStatus.ProviderError;
                return null;
            }

            // Assert that the user has been added to the store
            var newUser = GetUser(username, false);
            status = newUser == null ? MembershipCreateStatus.UserRejected : MembershipCreateStatus.Success;

            return newUser;
        }

        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            username = username.ToLower();
            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            Guid providerKey = GetPartitionKeyFromUsername(username);
            IEnumerable<user_entity> userEntities = userDataSource.GetUser(providerKey.ToString(), applicationName);
            if (userEntities == null)
                return false;

            user_entity userEntity = userEntities.FirstOrDefault();
            if (userEntity == null || (!deleteAllRelatedData && userEntity.is_deleted))
                return false;

            if (deleteAllRelatedData)
            {
                DeleteAccountPhisically(username, userDataSource, userEntity);
            }
            else
            {
                MarkAccountAsDeleted(userDataSource, userEntity);
            }

            return true;
        }

        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            IEnumerable<user_entity> userEntities = userDataSource.FindUsersByEmail(emailToMatch, applicationName);

            MembershipUserCollection users = new MembershipUserCollection();

            totalRecords = userEntities.Count();

            userEntities = userEntities.Take(pageSize * (pageIndex + 1));
            userEntities = userEntities.Skip(pageSize * (pageIndex));
            userEntities.OrderBy(x => x.username);
            foreach (user_entity userEntity in userEntities)
            {
                users.Add(GetUserFromEntity(userEntity));
            }

            return users;
        }

        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            usernameToMatch = usernameToMatch.ToLower();
            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            IEnumerable<user_entity> userEntities = userDataSource.FindUsers(usernameToMatch, applicationName);

            MembershipUserCollection users = new MembershipUserCollection();

            totalRecords = userEntities.Count();

            userEntities = userEntities.Take(pageSize * (pageIndex + 1));
            userEntities = userEntities.Skip(pageSize * (pageIndex));
            userEntities.OrderBy(x => x.username);
            foreach (user_entity userEntity in userEntities)
            {
                users.Add(GetUserFromEntity(userEntity));
            }

            return users;
        }

        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            IEnumerable<user_entity> userEntities = userDataSource.GetUsers(applicationName);

            MembershipUserCollection users = new MembershipUserCollection();

            totalRecords = userEntities.Count();

            userEntities = userEntities.Take(pageSize * (pageIndex + 1));
            userEntities = userEntities.Skip(pageSize * (pageIndex));
            foreach (user_entity userEntity in userEntities)
            {
                users.Add(GetUserFromEntity(userEntity));
            }

            return users;
        }

        //Users Online have special table. It includes username, applicationName and lastActivityDate.
        //Every inserted rows have the same ttl, so users online are the users, who are in UserOnlineEntity table.
        //UserIsOnlineTimeWindow is specified in config file.
        public override int GetNumberOfUsersOnline()
        {
            TimeSpan onlineSpan = new TimeSpan(0, System.Web.Security.Membership.UserIsOnlineTimeWindow, 0);
            DateTimeOffset compareTime = DateTimeOffset.Now.Subtract(onlineSpan);

            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            return userDataSource.GetNumberOfUsersOnline(compareTime, applicationName);
        }

        public override string GetPassword(string username, string answer)
        {
            if (!EnablePasswordRetrieval)
                throw new ProviderException("Password Retrieval Not Enabled.");

            if (PasswordFormat == MembershipPasswordFormat.Hashed)
                throw new ProviderException("Cannot retrieve Hashed passwords.");

            username = username.ToLower();
            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            user_entity userEntity = userDataSource.GetUser(GetPartitionKeyFromUsername(username).ToString(), applicationName).FirstOrDefault();

            if (userEntity == null)
                throw new MembershipPasswordException("The supplied user name is not found.");

            if (userEntity.is_locked_out)
                throw new MembershipPasswordException("The supplied user is locked out.");

            if (userEntity.is_deleted)
                throw new MembershipPasswordException("The supplied user does not exist.");

            if (RequiresQuestionAndAnswer && !CheckPassword(answer, userEntity.password_answer))
            {
                UpdateFailureCount(username, "passwordAnswer");

                throw new MembershipPasswordException("Incorrect password answer.");
            }

            string password = GetPasswordFormat(userEntity);

            userEntity.failed_password_answer_attempt_count = 0;
            userEntity.last_activity_date = DateTimeOffset.Now;
            userDataSource.Update(userEntity, _userIsOnlineTimeWindow, _userIsActiveTimeWindow);

            return password;
        }

        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            username = username.ToLower();
            return GetUser(GetPartitionKeyFromUsername(username), userIsOnline);
        }

        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            string providerUserKeyString;
            try
            {
                providerUserKeyString = ((Guid)providerUserKey).ToString();
            }
            catch (Exception)
            {
                return null;
            }
            IEnumerable<user_entity> userEntities = userDataSource.GetUser(providerUserKeyString, applicationName);

            user_entity userEntity;

            userEntity = userEntities.FirstOrDefault();

            if (userEntity == null)
                return null;
            userEntity.last_activity_date = DateTimeOffset.Now;
            userDataSource.Update(userEntity, _userIsOnlineTimeWindow);
            return GetUserFromEntity(userEntity);
        }

        public override string GetUserNameByEmail(string email)
        {
            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            IEnumerable<user_entity> userEntities = userDataSource.GetUsersByEmail(email, applicationName);
            user_entity userEntity = userEntities == null ? null : userEntities.FirstOrDefault();

            return userEntity == null ? String.Empty : userEntity.username;
        }

        public override void Initialize(string name, NameValueCollection config)
        {
            // Initialize values from web.config.
            if (config == null)
                throw new ArgumentNullException("config");

            if (String.IsNullOrEmpty(name))
                name = "CustomMembershipProvider";

            if (String.IsNullOrEmpty(config["description"]))
            {
                config.Remove("description");
                config.Add("description", "Custom Membership provider");
            }

            // Initialize the abstract base class.
            base.Initialize(name, config);
            _connectionString = GetConfigValue(ConfigurationManager.AppSettings["ConnectionString"], "localhost");

            SetMembershipSettings(config);

            DeterminePasswordFormatSettings(config);

            // Get encryption and decryption key information from the configuration.
            Configuration cfg = WebConfigurationManager.OpenWebConfiguration(System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath);
            machineKey = (MachineKeySection)cfg.GetSection("system.web/machineKey");

            if (machineKey.ValidationKey.Contains("AutoGenerate"))
                if (PasswordFormat != MembershipPasswordFormat.Clear)
                    throw new ProviderException("Hashed or Encrypted passwords are not supported with auto-generated keys.");
            _replicationFactor = Convert.ToInt32(GetConfigValue(ConfigurationManager.AppSettings["ReplicationFactor"], "1"));
        }

        public override string ResetPassword(string username, string answer)
        {
            if (!EnablePasswordReset)
                throw new NotSupportedException("Password reset is not enabled.");

            if (answer == null && RequiresQuestionAndAnswer)
            {
                UpdateFailureCount(username, "passwordAnswer");

                throw new ProviderException("Password answer required for password reset.");
            }

            const int newPasswordLength = 8;
            string newPassword = System.Web.Security.Membership.GeneratePassword(newPasswordLength, MinRequiredNonAlphanumericCharacters);

            ValidatePasswordEventArgs args = new ValidatePasswordEventArgs(username, newPassword, true);

            OnValidatingPassword(args);

            CheckValidatingPasswordArgs(args);

            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            IEnumerable<user_entity> userEntities = userDataSource.GetUser(GetPartitionKeyFromUsername(username).ToString(), applicationName);

            user_entity userEntity = userEntities.FirstOrDefault();
            if (userEntity == null || userEntity.is_deleted)
                throw new MembershipPasswordException("The supplied user name is not found.");

            if (userEntity.is_locked_out)
                throw new MembershipPasswordException("The supplied user is locked out.");

            var passwordAnswer = userEntity.password_answer;

            if (RequiresQuestionAndAnswer && !answer.Trim().Equals(passwordAnswer.Trim(),
                StringComparison.InvariantCultureIgnoreCase))
            {
                UpdateFailureCount(username, "passwordAnswer");

                throw new MembershipPasswordException("Incorrect password answer.");
            }

            userEntity.password = EncodePassword(newPassword);
            userEntity.last_password_changed_date = userEntity.last_activity_date = DateTimeOffset.Now;
            userDataSource.Update(userEntity, _userIsOnlineTimeWindow, _userIsActiveTimeWindow);

            return newPassword;
        }

        public override bool UnlockUser(string username)
        {
            username = username.ToLower();
            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            Guid providerKey = GetPartitionKeyFromUsername(username);

            user_entity userEntity = userDataSource.GetUser(providerKey.ToString(), applicationName).FirstOrDefault();
            if (userEntity == null || userEntity.is_deleted)
                return false;

            userEntity.is_locked_out = false;
            userEntity.last_activity_date = DateTimeOffset.Now;
            userDataSource.Update(userEntity, _userIsOnlineTimeWindow, _userIsActiveTimeWindow);

            return true;
        }

        public override void UpdateUser(MembershipUser user)
        {
            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            Guid providerKey = GetPartitionKeyFromUsername(user.UserName.ToLower());
            IEnumerable<user_entity> userEntities = userDataSource.GetUser(providerKey.ToString(), applicationName);

            user_entity userEntity = userEntities.FirstOrDefault();
            if (userEntity == null || userEntity.is_deleted)
                throw new MembershipPasswordException("The supplied user name is not found.");

            if (userEntity.is_locked_out)
                throw new MembershipPasswordException("The supplied user is locked out.");

            if (RequiresUniqueEmail && GetUserNameByEmail(user.Email) != userEntity.username)
            {
                throw new MembershipPasswordException("Duplicate email");
            }

            userEntity.email = user.Email;
            userEntity.comment = user.Comment;
            userEntity.is_approved = user.IsApproved;
            userDataSource.Update(userEntity, _userIsOnlineTimeWindow, _userIsActiveTimeWindow);
        }

        public override bool ValidateUser(string username, string password)
        {
            username = username.ToLower();
            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            Guid providerKey = GetPartitionKeyFromUsername(username);
            IEnumerable<user_entity> userEntities = userDataSource.GetUser(providerKey.ToString(), applicationName);

            if (userEntities == null) return false;
            user_entity userEntity = userEntities.FirstOrDefault();

            if (userEntity == null || userEntity.is_deleted || userEntity.is_locked_out) return false;

            bool isValid = false;
            bool isApproved = userEntity.is_approved;
            string pwd = userEntity.password;

            if (CheckPassword(password, pwd))
            {
                userEntity.last_activity_date = DateTimeOffset.Now;

                if (isApproved)
                {
                    isValid = true;

                    userEntity.last_login_date = DateTimeOffset.Now;
                    userEntity.failed_password_attempt_count = 0;
                }

                userDataSource.Update(userEntity, _userIsOnlineTimeWindow, _userIsActiveTimeWindow);
            }
            else UpdateFailureCount(username, "password");

            return isValid;
        }

        #endregion | Public Methods |

        #region | Private Methods |

        private void DeleteAccountPhisically(string username, CassandraTableStorageDataSource userDataSource, user_entity userEntity)
        {
            userDataSource.Delete(userEntity);    // Physically delete the account
            IEnumerable<role_user_entity> roles = userDataSource.GetRolesForUser(username, ApplicationName);
            foreach (role_user_entity role in roles)
            {
                userDataSource.Delete(role);
            }
        }

        private void DeterminePasswordFormatSettings(NameValueCollection config)
        {
            var tempFormat = config["passwordFormat"] ?? "Hashed";
            switch (tempFormat)
            {
                case "Hashed":
                    passwordFormat = MembershipPasswordFormat.Hashed;
                    break;

                case "Encrypted":
                    passwordFormat = MembershipPasswordFormat.Encrypted;
                    break;

                case "Clear":
                    passwordFormat = MembershipPasswordFormat.Clear;
                    break;

                default:
                    throw new ProviderException("Password format not supported.");
            }
        }

        private void MarkAccountAsDeleted(CassandraTableStorageDataSource userDataSource, user_entity userEntity)
        {
            // Only mark the account as deleted, don't physically delete it
            userEntity.is_deleted = true;
            userEntity.last_activity_date = DateTimeOffset.Now;
            userDataSource.Update(userEntity, _userIsOnlineTimeWindow, _userIsActiveTimeWindow);
        }

        private static void CheckValidatingPasswordArgs(ValidatePasswordEventArgs args)
        {
            if (args.Cancel)
                if (args.FailureInformation != null)
                    throw args.FailureInformation;
                else
                    throw new MembershipPasswordException("Reset password canceled due to password validation failure.");
        }

        /// <summary>
        /// A helper function to retrieve config values from the configuration file
        /// </summary>
        /// <param name="configValue"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        private static string GetConfigValue(string configValue, string defaultValue)
        {
            string a = String.IsNullOrEmpty(configValue) ? defaultValue : configValue;
            return String.IsNullOrEmpty(configValue) ? defaultValue : configValue;
        }

        /// <summary>
        /// Converts a hexadecimal string to a byte array. Used to convert encryption key values from the configuration
        /// </summary>
        /// <param name="hexString"></param>
        /// <returns></returns>
        private static byte[] HexToByte(string hexString)
        {
            var returnBytes = new byte[hexString.Length / 2];

            for (var i = 0; i < returnBytes.Length; i++)
                returnBytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);

            return returnBytes;
        }

        /// <summary>
        /// Compares password values based on the MembershipPasswordFormat
        /// </summary>
        /// <param name="password">Cleartext password</param>
        /// <param name="dbpassword">Encoded or hashed password</param>
        /// <returns>True if same values</returns>
        private bool CheckPassword(string password, string dbpassword)
        {
            string pass1 = password;
            string pass2 = dbpassword;

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Encrypted:
                    pass2 = DecodePassword(dbpassword);
                    break;

                case MembershipPasswordFormat.Hashed:
                    pass1 = EncodePassword(password);
                    break;

                default:
                    break;
            }

            return pass1 == pass2;
        }

        /// <summary>
        /// Decrypts or leaves the password clear based on the PasswordFormat
        /// </summary>
        /// <param name="encodedPassword">Password to decode</param>
        /// <returns>The decoded password</returns>
        private string DecodePassword(string encodedPassword)
        {
            string password = encodedPassword;

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Clear:
                    break;

                case MembershipPasswordFormat.Encrypted:
                    password = Encoding.Unicode.GetString(DecryptPassword(Convert.FromBase64String(password)));
                    break;

                case MembershipPasswordFormat.Hashed:
                    throw new ProviderException("Cannot decode a hashed password.");
                default:
                    throw new ProviderException("Unsupported password format.");
            }

            return password;
        }

        /// <summary>
        /// Encrypts, Hashes, or leaves the password clear based on the PasswordFormat
        /// </summary>
        /// <param name="password">Password</param>
        /// <returns>Encoded password</returns>
        private string EncodePassword(string password)
        {
            var encodedPassword = password;

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Clear:
                    break;

                case MembershipPasswordFormat.Encrypted:
                    encodedPassword = Convert.ToBase64String(EncryptPassword(Encoding.Unicode.GetBytes(password)));
                    break;

                case MembershipPasswordFormat.Hashed:
                    /* I don't use machineKey.ValidationKey as a hash key since it changes for each port which makes it useless in an development environment
                    , but you probably should in a production environment.*/
                    var hash = new HMACSHA1 { Key = HexToByte(ConfigurationManager.AppSettings["StaticKey"]) };
                    encodedPassword = Convert.ToBase64String(hash.ComputeHash(Encoding.Unicode.GetBytes(password)));
                    break;

                default:
                    throw new ProviderException("Unsupported password format.");
            }

            return encodedPassword;
        }

        /// <summary>
        /// The provider key is a Guid made from the username MD5 hash
        /// </summary>
        /// <param name="username">User name</param>
        /// <returns>Provider key</returns>
        private Guid GetPartitionKeyFromUsername(string username)
        {
            UnicodeEncoding unicodeEncoding = new UnicodeEncoding();
            byte[] message = unicodeEncoding.GetBytes(username);
            MD5 hashString = new MD5CryptoServiceProvider();
            return new Guid(hashString.ComputeHash(message));
        }

        private string GetPasswordFormat(user_entity userEntity)
        {
            string password;
            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Encrypted:
                    password = DecodePassword(userEntity.password);
                    break;

                case MembershipPasswordFormat.Clear:
                    password = userEntity.password;
                    break;

                default:
                    throw new MembershipPasswordException("Only encrypted or plaintext passwords can be retrieved.");
            }
            return password;
        }

        private MembershipUser GetUserFromEntity(user_entity userEntity)
        {
            return new MembershipUser(
                    this.Name,
                    userEntity.username,
                    new Guid(userEntity.partition_key),
                    userEntity.email,
                    userEntity.password_question,
                    userEntity.comment,
                    userEntity.is_approved,
                    userEntity.is_locked_out,
                    userEntity.creation_date.DateTime,
                    userEntity.last_login_date.DateTime,
                    userEntity.last_activity_date.DateTime,
                    userEntity.last_password_changed_date.DateTime,
                    userEntity.last_locked_out_date.DateTime
                );
        }

        private void SendAccountLockedEmail(Guid providerKey, string userName, string emailAddress)
        {
            // Now lets create an email message
            var emailMessage = new StringBuilder();
            string header = "Your " + ApplicationName + " account has been locked out!";
            emailMessage.Append(header);
            emailMessage.Append(string.Format("Hello, {0}.<br />Someone, possibly you, have {1} bad sign in attempts to your " + ApplicationName + " account. Because of this your account was automatically locked out to prevent a possible account password hacking attempt. ", userName, MaxInvalidPasswordAttempts));
            emailMessage.Append("You can't sign in to " + ApplicationName + " until your account is unlocked. To unlock your " + ApplicationName + " account again please click the link below:<br /><br />");
            string url = string.Format("http://www.MyApp.com/Account/UnlockAccount.aspx?Id={0}", providerKey);
            emailMessage.Append(string.Format("<a href='{0}'>{0}</a><br /><br />", url));
            emailMessage.Append("If your e-mail client does not allow links then copy & paste the above url into your browser address bar.");
            string footer = String.Format("This e-mail was sent your user account at <a href=\"http://www." + ApplicationName + ".com\">www." + ApplicationName + ".com</a> was locked out because of too many bad sign attempt.");
            emailMessage.Append(footer);
            string fromEmail = ConfigurationManager.AppSettings["fromEmail"];
            string password = ConfigurationManager.AppSettings["AdminPassword"];

            IEmailSender emailSender = new EmailSender();
            emailSender.SendAsyncEmail(fromEmail, emailAddress, password, "Unlock your MyApp account", emailMessage.ToString());
        }

        private void SetMembershipSettings(NameValueCollection config)
        {
            applicationName = GetConfigValue(config["applicationName"], System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath);
            maxInvalidPasswordAttempts = Convert.ToInt32(GetConfigValue(config["maxInvalidPasswordAttempts"], "5"));
            passwordAttemptWindow = Convert.ToInt32(GetConfigValue(config["passwordAttemptWindow"], "10"));
            minRequiredNonAlphanumericCharacters = Convert.ToInt32(GetConfigValue(config["minRequiredNonAlphanumericCharacters"], "1"));
            minRequiredPasswordLength = Convert.ToInt32(GetConfigValue(config["minRequiredPasswordLength"], "7"));
            passwordStrengthRegularExpression = Convert.ToString(GetConfigValue(config["passwordStrengthRegularExpression"], ""));
            enablePasswordReset = Convert.ToBoolean(GetConfigValue(config["enablePasswordReset"], "true"));
            enablePasswordRetrieval = Convert.ToBoolean(GetConfigValue(config["enablePasswordRetrieval"], "true"));
            requiresQuestionAndAnswer = Convert.ToBoolean(GetConfigValue(config["requiresQuestionAndAnswer"], "false"));
            requiresUniqueEmail = Convert.ToBoolean(GetConfigValue(config["requiresUniqueEmail"], "true"));

            _userIsActiveTimeWindow = Convert.ToInt32(GetConfigValue(ConfigurationManager.AppSettings["UserIsActiveTimeWindow"], "3"));

            if (!int.TryParse(GetConfigValue(ConfigurationManager.AppSettings["UserIsOnlineTimeWindow"], "15"), out _userIsOnlineTimeWindow)) _userIsOnlineTimeWindow = 15;
        }

        /// <summary>
        /// A helper method that performs the checks and updates associated with password failure tracking
        /// </summary>
        /// <param name="username">Username</param>
        /// <param name="failureType">Failure type; "password" or "passwordAnswer" accepted</param>
        private void UpdateFailureCount(string username, string failureType)
        {
            CassandraTableStorageDataSource userDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            Guid providerKey = GetPartitionKeyFromUsername(username);

            user_entity userEntity = userDataSource.GetUser(providerKey.ToString(), applicationName).FirstOrDefault();
            if (userEntity == null || userEntity.is_deleted) return;

            DateTimeOffset windowStart = new DateTimeOffset();
            int failureCount = 0;

            switch (failureType)
            {
                case "password":
                    failureCount = userEntity.failed_password_attempt_count;
                    windowStart = userEntity.failed_password_attempt_window_start;
                    break;

                case "passwordAnswer":
                    failureCount = userEntity.failed_password_answer_attempt_count;
                    windowStart = userEntity.failed_password_answer_attempt_window_start;
                    break;
            }

            DateTimeOffset windowEnd = windowStart.AddMinutes(PasswordAttemptWindow);

            if (failureCount == 0 || DateTimeOffset.Now > windowEnd)
            {
                // First password failure or outside of PasswordAttemptWindow.
                // Start a new password failure count from 1 and a new window starting now.
                switch (failureType)
                {
                    case "password":
                        userEntity.failed_password_attempt_count = 1;
                        userEntity.failed_password_attempt_window_start = DateTimeOffset.Now;
                        break;

                    case "passwordAnswer":
                        userEntity.failed_password_answer_attempt_count = 1;
                        userEntity.failed_password_answer_attempt_window_start = DateTimeOffset.Now;
                        break;
                }
            }
            else
            {
                if (failureCount++ >= MaxInvalidPasswordAttempts)
                {
                    // Password attempts have exceeded the failure threshold. Lock out the user.
                    userEntity.is_locked_out = true;
                    userEntity.last_locked_out_date = DateTimeOffset.Now;

                    // Notify that the user has been locked out by e-mail
                    SendAccountLockedEmail(providerKey, userEntity.username, userEntity.email);
                }
                else
                {
                    // Password attempts have not exceeded the failure threshold. Update
                    // the failure counts. Leave the window the same.
                    switch (failureType)
                    {
                        case "password":
                            userEntity.failed_password_attempt_count = failureCount;
                            break;

                        case "passwordAnswer":
                            userEntity.failed_password_answer_attempt_count = failureCount;
                            break;
                    }
                }
            }

            userDataSource.Update(userEntity, _userIsOnlineTimeWindow, _userIsActiveTimeWindow);
        }

        #endregion | Private Methods |
    }
}